﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Device.Location;
using System.Diagnostics;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Threading;
using OneBusAway.WP7.ViewModel.BusServiceDataStructures;
using OneBusAway.WP7.ViewModel.Data;

namespace OneBusAway.WP7.ViewModel
{
    public class StopsMapVM : AViewModel
    {
        private Object stopsForLocationCompletedLock;
        private Object stopsForLocationLock;
        private GeoCoordinate previousQuery;

        private ICache<string, Stop> stopsCache;

        // this is just a map of id -> stop that mirrors StopsForLocation.
        // used to do lookups.  too bad we can't bind directly to this object's Values property.
        private IDictionary<string, Stop> stopsForLocationIndex;

        #region Constructors

        public StopsMapVM()
            : base()
        {
            Initialize();
        }

        public StopsMapVM(IBusServiceModel busServiceModel, IAppDataModel appDataModel)
            : base(busServiceModel, appDataModel)
        {
            Initialize();
        }

        private void Initialize()
        {
            stopsForLocationCompletedLock = new Object();
            stopsForLocationLock = new Object();
            stopsForLocationIndex = new Dictionary<string, Stop>();
            StopsForLocation = new ObservableCollection<Stop>();

            stopsCache = CacheFactory.Singleton.StopsCache;
            previousQuery = new GeoCoordinate();
        }

        #endregion

        #region Public Methods/Properties

        public ObservableCollection<Stop> StopsForLocation { get; private set; }
        public String CacheCount { get; private set; }

        public void LoadStopsForLocation(GeoCoordinate center)
        {
            // If the two queries are being rounded to the same coordinate, no 
            // reason to re-parse the data out of the cache
            if (busServiceModel.AreLocationsEquivalent(previousQuery, center) == true)
            {
                return;
            };

            this.LoadingText = "Loading stops";
            operationTracker.WaitForOperation("StopsForLocation");

            // pull in cached data before firing off a background query
            IDictionary<string, Stop> tempStops = new Dictionary<string,Stop>();
            foreach (Pair<string, Stop> stopPair in stopsCache.GetAll())
            {
                if (stopPair.value.location.GetDistanceTo(center) <= defaultSearchRadius)
                {
                    tempStops.Add(stopPair.key, stopPair.value);
                    stopsCache.Get(stopPair.key); // this Get is just to update the LRU information in the cache to keep this stop warm
                }
            }

            // update the view with the cached data
            UIAction(() =>
            {
                CacheCount = String.Format("DEBUG: {0} stops from in-memory cache", tempStops.Count);
                OnPropertyChanged("CacheCount");
            });
            SetStopsForLocation(tempStops);

            previousQuery = center;
            // and make the service call in the background
            busServiceModel.StopsForLocation(center, defaultSearchRadius);
        }

        /// <summary>
        /// Sets the contents of StopsForLocation to the Values of the specified Dictionary.
        /// </summary>
        /// <remarks>
        /// Implementation does not clear StopsForLocation (so as not to clear the screen).
        /// Instead, removes all stops that don't belong and adds ones that do.
        /// </remarks>
        /// <param name="newStops"></param>
        private void SetStopsForLocation(IDictionary<string,Stop> newStops)
        {
            // .NET for the phone doesn't support HashSet.... Dictionary is a poor-man's hashset
            // linear pass to calculate the set of stops to remove
            IDictionary<string, Stop> stopsToRemove = new Dictionary<string, Stop>();
            lock (stopsForLocationLock)
            {
                foreach (string stopId in stopsForLocationIndex.Keys)
                {
                    if (!newStops.ContainsKey(stopId))
                    {
                        stopsToRemove.Add(stopId, stopsForLocationIndex[stopId]);
                    }
                }
            }
            // O(n^2) pass to remove them.  Too bad there isn't an ObservableSet
            UIAction(() =>
            {
                lock (stopsForLocationLock)
                {
                    foreach (Stop s in stopsToRemove.Values)
                    {
                        StopsForLocation.Remove(s);
                        stopsForLocationIndex.Remove(s.id);
                    }
                }
            });
            

            // and a linear pass to add any new stops
            UIAction(() =>
            {
                lock (stopsForLocationLock)
                {
                    foreach (Stop s in newStops.Values)
                    {
                        if (!stopsForLocationIndex.ContainsKey(s.id))
                        {
                            StopsForLocation.Add(s);
                            stopsForLocationIndex.Add(s.id, s);
                        }
                    }
                }
            });
        }

        public override void RegisterEventHandlers(Dispatcher dispatcher)
        {
            base.RegisterEventHandlers(dispatcher);

            this.busServiceModel.StopsForLocation_Completed += new EventHandler<EventArgs.StopsForLocationEventArgs>(busServiceModel_StopsForLocation_Completed);
        }

        public override void UnregisterEventHandlers()
        {
            base.UnregisterEventHandlers();

            this.busServiceModel.StopsForLocation_Completed -= new EventHandler<EventArgs.StopsForLocationEventArgs>(busServiceModel_StopsForLocation_Completed);

            // Reset loading to 0 since event handlers have been unregistered
            this.operationTracker.ClearOperations();
        }

        #endregion

        #region Event Handlers

        void busServiceModel_StopsForLocation_Completed(object sender, EventArgs.StopsForLocationEventArgs e)
        {
            Debug.Assert(e.error == null);

            if (e.error == null)
            {
                // this lock simply prevents multiple instances of this handler from running simultaneously
                lock (stopsForLocationCompletedLock)
                {
                    IDictionary<string, Stop> newStops = new Dictionary<string, Stop>();
                    foreach(Stop s in e.stops)
                    {
                        newStops.Add(s.id, s);
                        stopsCache.Put(s.id, s);
                    }

                    SetStopsForLocation(newStops);
                }
            }
            else
            {
                ErrorOccured(this, e.error);
            }

            UIAction(() =>
            {
                CacheCount = "DEBUG: All stops from the service.";
                OnPropertyChanged("CacheCount");
            });

            operationTracker.DoneWithOperation("StopsForLocation");
        }

        #endregion

    }
}
